"""
 Title: Scraper.py
 Author: Matthew Tanner
 Project: Facultimes
 This is an Oregon State University project built as a requirement for CS319. 
 The project is a collaboration between Matthew Tanner, Arnold Baghomian, and
 Joseph Helgeson. 
 """

from bs4 import BeautifulSoup

import requests
import datetime
from datetime import datetime, timedelta
import copy
import sys
import HTMLParser

class Scraper:
	"""The Scraper class obtains data from the OSU Online Schedule of classes.

	Scrapes class schedules for given username (instructor) from Oregon State 
	University online schedule of classes. The returned data is used in the 
	Scheduler class to provide scheduling solutions. 

	It should be noted that the route into the page containing individual 
	class sections involves using an exact name match for the instructor. 
	The valid names come from an html select. See this link: 

		http://catalog.oregonstate.edu/SOCSearcher.aspx?wks=&chr=abcdeop

	A utility, 'instructors.py' is provided in the main application folder,
	which scrapes all valid names and saves to txt file.

    Attributes: 
    	All class attributes are meant to be private
    """

	# Convert name from 'First Last' to 'Last, FirstInit'
	def __initialFirst(self, str):
		s = str[:]
		for i in range(0, len(s)):
			if s[i] == ",":
				s = s[:i+3] + "."
				break
		return s

	# Make request and return response
	def __makeRequest(self, url):
		try:
			r = requests.get("http://" + url)
		except requests.ConnectionError as err:
			self.schedulerObj.error = ("Network Error: Error connecting to "
				"OSU schedule: '%s'" % url)
			return -1; 
		return r


	# scrape instructor id
	def __getInstId(self, inst_name):

		inst_id = None
		first_letter = inst_name[0].upper()
		url = "catalog.oregonstate.edu/SOCSearcher.aspx?isa="+first_letter+"%&wks=&chr=abcdeop"

		r = self.__makeRequest(url)
		if r == -1:
			return -1;
		try:
			data = r.text
			soup = BeautifulSoup(data)

			for element in soup.find_all('option'):
				id = element.get('value')
				id = id.strip()
				name = element.get_text('value').strip().lower()
				if inst_name == name:
					inst_id = id
					break
		except (HTMLParser.HTMLParseError, ImportError, KeyError, AttributeError) as err:
			self.schedulerObj.error = ("Error parsing OSU schedule: %s: %s:" % (url, str(err)))
			return -1

		if inst_id == None:
			self.schedulerObj.error = ("Error: User: '%s' not found in OSU "
				"schedule" % inst_name)
			return -1
		return inst_id

	# with instructor name, get course links
	def __getCourseLinks(self, inst_id, inst_name):
		campuses = ["Corvallis", "Cascades", "Ecampus", "HMSC"]
		links = []

		for campus in campuses:
			url = "catalog.oregonstate.edu/SOCSearcher.aspx?cmp=" + campus + "&wks=&ist=" + inst_id + "&chr=abcder"

			# r = requests.get("http://" + url)
			r = self.__makeRequest(url)
			if r == -1:
				return -1;
			try:
				data = r.text
				soup = BeautifulSoup(data)
				for element in soup.find_all('a'):
					link = element.get('href')
					if link is not None:
						if len(link) > 6:
							if link[:7] == "/Course":
								links.append(link)
			except (HTMLParser.HTMLParseError, ImportError, KeyError, AttributeError) as err:
				self.schedulerObj.error = ("Error parsing OSU schedule: %s: %s:" % (url, str(err)))
				return -1				
		return links


	# get each section time/dates for each course
	def __getCourses(self, links, inst_name):
		courses = []

		for link in links:
			url = "catalog.oregonstate.edu" + link
			r = self.__makeRequest(url)
			if r == -1:
				return -1;
			try:
				data = r.text
				soup = BeautifulSoup(data)
				table = soup.find("table", {"id": "ctl00_ContentPlaceHolder1_SOCListUC1_gvOfferings"})
				for row in table.find_all("tr"):
					name_field = row.contents[6]
					name_text = name_field.get_text("td").strip().lower()

					if name_text == self.__initialFirst(inst_name):
						time_text = row.contents[7].get_text("*").strip()
						courses.append(time_text)
			except (HTMLParser.HTMLParseError, ImportError, KeyError, AttributeError) as err:
				self.schedulerObj.error = ("Error parsing OSU schedule: %s: %s:" % (url, str(err)))
				return -1	

		if courses == []:
			return None
		else:
			return courses

	# Convert day of week from string to int
	def __parseDays(self, str):
		chars = list(str)
		dayInt = []
		for day in chars:
			if day.lower() == "u":
				dayInt.append(6)
			elif day.lower() == "m":
				dayInt.append(0)
			elif day.lower() == "t":
				dayInt.append(1)
			elif day.lower() == "w":
				dayInt.append(2)
			elif day.lower() == "r":
				dayInt.append(3)
			elif day.lower() == "f":
				dayInt.append(4)
			elif day.lower() == "s":
				dayInt.append(5)
			else:
				self.schedulerObj.error = ("Error parsing OSU schedule: Unknown "
					"weekday value: '%s'" % day)
				return -1
		return dayInt


	# split scraped string into string elements
	# input = "TR 0800-0950*6/23/14-8/15/14"
	#  or ... "TR 0800-0950*6/23/14-8/15/14*0800-0950*6/23/14-8/15/14*..."
	def __strToData(self, courses):		
		strElements = []
		try:				
			for string in courses:
				if string.lower() == "tba":
					continue
				s= string.split("*")

				# in this case:
				# [u'MWF 1200-1250', u'6/23/14-9/5/14', u'TR 1230-1350', u'6/23/14-9/5/14', ...]
				# seperate out pairs:
				# [u'MWF 1200-1250', u'6/23/14-9/5/14'], [u'TR 1230-1350', u'6/23/14-9/5/14'], ...
				strings = []
				for i in range(0, (len(s)/2)+1, 2): #0, 2, 4, ...
					strings.append([s[i], s[i+1]])
				# split into structured data strings ["TR", "0800", "0950", "6/23/14", "8/15/14"]
				for s in strings:
					daystr = s[0].split()
					timestr = daystr[1].split("-")
					datestr = s[1].split("-")
					course = [daystr[0], timestr[0], timestr[1], datestr[0], datestr[1]]
					strElements.append(course)

			# change split string to structured data 
			# input = ["TR", "0800", "0950", "6/23/14", "8/15/14"]
			courseData = []
			for element in strElements:
				days = self.__parseDays(element[0])
				if days == -1:
					return -1
				element[0] = days

				times = [element[1], element[2]]

				startDate = datetime.strptime(element[3], '%m/%d/%y')
				endDate = datetime.strptime(element[4], '%m/%d/%y')
				dates = [startDate, endDate]

				courseData.append([days, times, dates])
		except (IndexError, ValueError) as err:
			self.schedulerObj.error = ("Error parsing OSU schedule: %s:" % str(err))
			return -1
		if not courseData:
			return None
		else: 
			return courseData


	# with courseData, creates array of event dictionaries with format:
	# ["start": datetime_obj, "end": datetime_obj, "name": String]
	# courseData is [days, times, dates] 
	#           ex: [[2, 4], ["0800", "0950"], [startDate_obj, endDate_obj]]
	def __dataToEvents(self, courseData):
		self.events = []
		try:
			for course in courseData: 
				days = course[0]
				startHour = course[1][0][:2]
				startMin = course[1][0][2:]
				endHour = course[1][1][:2]
				endMin = course[1][1][2:]
				currDate = copy.deepcopy(course[2][0])
				endDate = course[2][1]
				# iterate through days from start to end date
				while currDate <= endDate:
					for d in days:			
						if currDate.weekday() == d:
							# if day of week matches course schedule, create event
							eventStart = copy.deepcopy(currDate)
							eventStart = eventStart.replace(hour=int(startHour), minute=int(startMin))
							eventEnd = copy.deepcopy(currDate)
							eventEnd = eventEnd.replace(hour=int(endHour), minute=int(endMin))
							# event = {"start": eventStart, "end": eventEnd, "name": inst_name}
							event = [eventStart, eventEnd, self.inst_name]
							# print event
							self.events.append(event)
					currDate = currDate + timedelta(days=1)	
		except (IndexError, ValueError) as err:
			self.schedulerObj.error = ("Error parsing OSU schedule: %s:" % str(err))
			return -1
		if not self.events:	
			return None
		else:
			return self.events


	def scrape(self, schedulerObj, inst_name):
		"""Scrape data from OSU Online Schedule

		Scrapes all of a given instructor's class schedules from OSU website. 
		The data scraped from the website is turned into a list of events 
		representing all time windows when the instructor is in class.

		Args:
			String inst_name: Instructor name
			Scheduler schedulerObj: schedulerObj is passed in so that
				error codes can be assigned to Scheduler.error.
		Returns:
			0 on success
			-1 on Error (sets Scheduler.error with error code)
		"""
		self.inst_name = inst_name
		self.schedulerObj = schedulerObj

		# get instructor ID
		inst_id = self.__getInstId(self.inst_name)
		if inst_id == -1:
			return -1

		links = self.__getCourseLinks(inst_id, self.inst_name)
		if links == -1:
			return -1
		if links == []:
			self.events = [[datetime(2000, 1, 1, 1, 1), datetime(2000, 1, 1, 1, 1), inst_name]]
			return 0

		courseStr = self.__getCourses(links, self.inst_name)
		if courseStr == None:
			elf.events = [[datetime(2000, 1, 1, 1, 1), datetime(2000, 1, 1, 1, 1), inst_name]]
			return 0
		elif not courseStr:
			return 0

		self.courseData = self.__strToData(courseStr)
		if self.courseData == None:
			self.events = [[datetime(2000, 1, 1, 1, 1), datetime(2000, 1, 1, 1, 1), inst_name]]
			return 0

		self.events = self.__dataToEvents(self.courseData)
		if self.events == None:
			self.events = [[datetime(2000, 1, 1, 1, 1), datetime(2000, 1, 1, 1, 1), inst_name]]
			return 0
		return 0

	def getEvents(self):
		"""Return list of events

		Returns list of events generated from scraped course schedule data.
		Args:
			None
		Returns:
			list of events: Each element as [datetime, datetime, username]
		"""
		if hasattr(self, 'events'):
			if self.events != [] and self.events != None:
				return self.events
		return [[datetime(2000, 1, 1, 1, 1), datetime(2000, 1, 1, 1, 1), inst_name]]

	def __init__(self):
		pass


# For testing purposes
# Can call this class on the command line with an instructor name as sole argument
def main():
	inst_name = raw_input("Enter a name in format lastname, firstname: ").lower()
	s = Scraper()
	res = s.scrape(inst_name)
	print res
	if res > 0:
		events = s.getEvents()
		if events is not None:	
			for e in events:
				print e[0] 
				print e[1] 
				print e[2]
				print
		else:
			print s.getEvents()

if __name__ == "__main__":
	main()
